return [[
{%
context.title = '开发说明'
%}
<div class="panel panel-default h5">
    <div class="panel-heading">应用结构</div>
    <div class="panel-body">
<p>目录及文件：</p>
<pre>
|____index.lua 						#入口文件
|____lua-releng 					#检查代码质量／是否使用了全局变量
|____config 						#配置文件目录
| |____db.lua 						#数据库配置
| |____lang.lua 					#语言包配置
| |____memcache.lua 					#memcache配置
| |____session.lua 					#session配置
| |____web.lua 						#网站基本信息配置
|____controllers 					#controller目录
| |____site.lua 					#前端页面
|____models 						#models目录
| |____LoginForm.lua 					#登录逻辑处理类
| |____Userinfo.lua 					#用户信息管理类
|____runtime 						#Openresty运行目录
| |____client_body_temp 				#post上传的临时保存目录
| |____fastcgi_temp 					#fastcig临时目录
| |____logs 						#站点日志目录
| | |____access.log 					#请求日志
| | |____error.log 					#错误日志
| | |____nginx.pid 					#nginx进程文件
| |____nginx.conf 					#站点nginx配置文件
| |____proxy_temp 					#proxy_temp
| |____scgi_temp 					#scgi_temp 
| |____start.sh 					#站点启动程序
| |____stop.sh 						#站点停止程序
| |____uwsgi_temp 					#uwsgi_temp
|____vendor 						#框架及第三方类
| |____ActiveRecord.lua 				#数据库处理基类
| |____Application.lua 					#请求处理类
| |____Controller.lua 					#Controller基类
| |____Files.lua 					#上传文件接收类
| |____Memcache.lua 					#Memcache操作类
| |____Model.lua 					#Model基类
| |____Mysql.lua 					#Mysql操作类
| |____Pager.lua 					#分页类
| |____Query.lua 					#查询构建器
| |____Request.lua 					#请求信息工具类
| |____resty 						#第三方resty工具
| | |____http.lua 					#http请求工具
| | |____template.lua 					#lua模版工具类
| |____Session.lua 					#Session操作类
| |____User.lua 					#用户信息操作类
| |____Util.lua 					#基本工具类
|____views 						#页面模版目录
| |____layout 						#页面框架目录
| | |____main.lua.html 					#基本站点框架
| |____site 						#站点页面目录
| | |____error.lua.html 				#错误信息页
| | |____guide.lua.html 				#开发说明页
| | |____index.lua.html 				#首页
| | |____login.lua.html 				#登录页
|____web 						#静态资源目录
| |____css 						#样式
| | |____bootstrap-theme.css 				#bootstrap
| | |____bootstrap-theme.min.css 			#bootstrap
| | |____bootstrap.css 					#bootstrap
| | |____bootstrap.min.css 				#bootstrap
| | |____site.css 					#站点样式
| |____js  						#javascript	
| | |____bootstrap.js 					#bootstrap
| | |____bootstrap.min.js 				#bootstrap
| | |____jquery.js 					#jquery
| | |____jquery.min.js 					#jquery
| | |____jquery.slim.js 				#jquery
| | |____jquery.slim.min.js 				#jquery
| |____favicon.ico 					#图标
| |____robots.txt 					#robots.txt
</pre>
<p>运行机制概述</p>
<pre>
每一次应用开始处理 HTTP 请求时，它都会进行一个近似的流程。

用户提交指向 入口脚本 index.lua 的请求
入口脚本会创建一个 应用(Application) 实例用于处理该请求，并加载 配置。
应用会通过 request（请求） 应用组件解析被请求的 路由。
应用创建一个 controller（控制器） 实例具体处理请求。
执行controller中的before()方法进行请求过滤
如果执行before()返回true，则继续执行 action（动作），否则终止 
动作会加载一个数据模型，一般是从数据库中加载。
动作会渲染一个 View（视图），并为其提供所需的数据模型。
渲染得到的结果会返回给 response（响应） 应用组件。
响应组件会把渲染结果发回给用户的浏览器。
</pre>

<p>调试模式：</p>
<pre>
访问 /lua/index 会使用lua_code_cache off的模式访问站点
访问 /lua/&lt;filepath&gt; 可调试对应的lua脚本
</pre>

<p>应用(Application)</p>
<pre>
每接收到一个 HTTP 请求，入口脚本 index.lua 会创建一个 应用(Application) 实例用于处理该请求
Application实例创建后，会保存覆盖全局变量ngx.ctx（此全局变量生命周期对应每个请求）

ngx.ctx = require("vendor.Application"):new()

Application在创建时，会加载网站的基本配置及基本工具作为属性，如：

ngx.ctx.web 相当于 require("config.web"),	 --站点配置信息表
ngx.ctx.lang 相当于 require("config.lang"),	 --语言包配置信息
ngx.ctx.session 相当于 require("config.session"), --站点Session类
ngx.ctx.user 相当于 require("vendor.User"),	 --站点用户工具类
ngx.ctx.request 相当于 require("vendor.Request"), --站点请求处理类

在执行Application的new()时，会创建一个继承于Application的Controller实例，
因此ngx.ctx即是一个Application也是一个Controller实例，并且controller中也可以通过
self.web,self.lang,self.session.self.user.self.request直接引用以上配置及工具

注意：在使用时，尽量少使用全局变量ngx.ctx，而应该通过函数传值的方式进行传递，能获得更快的运行速度
</pre>
<p>控制器（Controllers）:</p>
<pre>
local _M = require("vendor.Controller"):new{_VERSION='0.01'}	--生成Controller新实例

function _M:indexAction()	--动作方法名必须以name+Action组成
	if not self.user.isLogin then return self:loginAction() end		--使用用户信息类判断用户是否登录

	local Product = require('models.Product')  				--使用Product数据表操作类
	local query = Product.find().where{'in','product_id',self.user.resource} --fin()方法生成新的查询器

	local page = Pager{				--使用分页类
		pageSize = 8, 				--设置每页显示最多8条信息
		totalCount = query.count(),		--使用查询器查询符合条件的数据总条数
	}

	local list = query.orderBy('create_time desc').page(page).all() --使用查询器查询分页数据集
	
	return self:render('index',{
		page = page,
		list = list,
	})
end

retrun _M

如果保存为controllers/filename.lua，则访问?act=filename.index时，会执行上面对应的indexAction()方法
</pre> 
<p>模型（Models）</p>
<pre>
模型是 MVC 模式中的一部分， 是代表业务数据、规则和逻辑的对象。
可通过继承 "vendor.Model" 或它的子类定义模型类， 基类"vendor.Model"支持许多实用的特性，如：

local _M = require("vendor.Model"):new{_version='0.0.1'}	--生成Model新实例

local Userinfo = require('models.Userinfo')		--在Model里使用其他model

function _M.rules()		--方法rules添加数据校验规则
	return {
		{{'username','password','sacode'}, 'trim'},	--通过trim自动过滤输入
		{'username', 'required',message='请填写登录账号'},	--通过required规则设置必须填写
		--{'username', 'email'}, 			--如果需要用户名必须为email时设置
		{'password', 'required',message='请填写登录密码'},
		--使用自定义方法校验参数
		{'password','checkPass'}		--使用自定义checkPass方法进行校验
	}
end

function _M:checkPass(key)
	if self:hasErrors() then return end

	local user = Userinfo.getUserByName(self.username)
	if not user then 
		self:addError('username','账号不存在')
	elseif user.password ~= self.password then
		self:addError('password','密码错误')
	else
		self.userinfo = user
	end
end

function _M:login(user)
	if not self:validate() then return false end
	
	return user.login(self.userinfo, rememberMe and 3600*24*30 or 0)
end

return _M


在Controller里使用Model：

local model = require('models.LoginForm'):new()		--因为Model包含有数据，一定要调用new方法生成新实例，避免出现数据缓存问题

--通过model的load()方法，可自动装载数据
if model:load(self.request.post()) and model:login(self.user) then
	return self:goHome()
end
</pre>
<p>视图（Views）</p>
<pre>
视图基于lua-resty-template（https://github.com/bungle/lua-resty-template）实现

在视图中必须使用以下标签：
&#123;&#123;expression&#125;&#125;, 输出expression表达式，并用经过html格式化
&#123;*expression*&#125;, 原样输出expression表达式的结果
&#123;% lua code %&#125;, 执行Lua代码
&#123;# comments #&#125;, 所有&#123;＃和＃&#125;之间的内容都被认为是注释掉的（即不输出或执行）

与lua-resty-template的普通用法不同，所有视图文件，以lua文件的形式保存在views子目录下，文件名为*.lua.html
controller中的render方法渲染试图时，会以require的形式去获取视图内容

views/layout 目录存放框架视图，渲染视图时默认使用views/layout/main.lua.html，
controller中可通过layout属性设置使用不同的框架视图，如：
local _M = require("vendor.Controller"):new{layout = 'manage'}

views下的其他子目录分别为不同功能模块对应的内容视图，所有页头，页尾，菜单等内容应在框架视图中实现
内容视图中不推荐使用&#123;(template)&#125;的形式来包含其他视图，因与默认渲染方式不同，会导致找不到文件等错误

controller在渲染视图时，会将所有application及controller的数据传递给视图，因此在视图中可以直接使用这些数据
如：&#123;&#123;lang.siteName&#125;&#125;可输出语言包中配置的网站名称

视图中可以通过设置context属性的方式用于内容视图跟框架视图之间传值，如设置：
&#123;% context.title = '开发说明' %&#125;
则可以在框架视图中&#123;&#123;title&#125;&#125;输出显示
</pre>
    </div>
    <div class="panel-heading">请求处理</div>
    <div class="panel-body">
<p>获取请求参数</p>
<pre>
local request = require("vendor.Request")
// 在Controller方法里可以直接通过self.request调用

local get = request.get()
// 等价于php: $get = $_GET;

local id = request.get('id');   
// 等价于php: $id = isset($_GET['id']) ? $_GET['id'] : null;

local id = request.get('id', 1)   
// 等价于php: $id = isset($_GET['id']) ? $_GET['id'] : 1;

local post = request.post()
// 等价于php: $post = $_POST;

local name = request.post('name')   
// 等价于php: $name = isset($_POST['name']) ? $_POST['name'] : null;

local name = request.post('name', '')   
// 等价于php: $name = isset($_POST['name']) ? $_POST['name'] : '';

local cookie = request.cookie()
// 等价于php: $cookie = $_COOKIE;

local sso = request.cookie('sso')   
// 等价于php: $sso = isset($_COOKIE['sso']) ? $_COOKIE['sso'] : null;

local sso = request.cookie('sso', '')   
// 等价于php: $sso = isset($_COOKIE['sso']) ? $_COOKIE['sso'] : '';

--判断是否有上传文件
ngx.say(request.isPostFile())
// 注意跟php不同的地方，如果有上传文件时，普通参数是无法通过request.post()方法获取到的
// 建议上传文件时，普通参数通过GET传递

--接收上传文件
local file = require('vendor.Files')
local savename = path..filename

local ok,err = file.receive('upfile',savename)	--接收name为upfile的上传文件
if not ok  then  
	return {retcode=1,retmsg='接收文件失败，请重试:'..err}  
end

</pre>

<p>设置cookie：</p>
<pre>
local util = require "vendor.Util"
util.set_cookie('abc','123')
util.set_cookie('def','456',3600)
util.set_cookie(name,value,expire,path,domain,secure,httponly)
</pre>

<p>Session操作：</p>
<pre>
local session = require "vendor.Session"
// 在Controller方法里可以直接通过self.session调用
--session.start()	--启用session，会去设置一个_sessionid的cookie，此操作可以省略
--session.exptime = 1800 --修改session失效时间时，默认30分钟
--session.cache = ngx_shared['session_cache'] --默认使用ngx的缓存，对应runtime/nginx.conf里的lua_shared_dict配置项
--session.cache = require("vendor.Memcache"):new{host="127.0.0.1",port="11211"} --使用memcached缓存打开此配置

local sessions = session.get()
// 等价于php: $sessions = $_SESSION;

local abc = session.get('abc')   
--等价于:local abc = session.abc 
--等价于:local abc = session['abc']
// 等价于php: $abc = isset($_SESSION['abc']) ? $_SESSION['abc'] : null;

local abc = session.get('abc', '')   
// 等价于php: $abc = isset($_SESSION['abc']) ? $_SESSION['abc'] : '';

//设置session值
session.set('abc','abc-value')
--等价于:session.abc = 'abc-value'
--等价于:session['abc'] = 'abc-value'
</pre>
    </div>
    <div class="panel-heading">数据库操作（Working with Databases）</div>
    <div class="panel-body">
<p>数据库访问 (DAO)</p>
<pre>
local db = require('vendor.Mysql'):new{
    host = "127.0.0.1",
    port = 3306,
    database = "mytest",
    user = "root",
    password = "",
    charset = "utf8", --建议配置Mysql的默认连接字符集为utf8，可去掉此配置项，此项配置会增加一次'SET NAMES utf8'的操作
}

或者直接通过配置文件获得：
local db = require('config.db')

执行sql：
local rows = db:query('select * from mytable')
ngx.say(#rows)
</pre>
<p>使用查询生成器 (Query Builder)</p>
<pre>
vendor.Query 封装了 vendor.Mysql，并提供快捷构建 安全 查询语句的方法，例如：

local db = require('config.db')
local query = require('vendor.Query')()
local rows = query.use(db).from('products').where({product_id=123}).all()
--相当于执行local rows = db:query("select * from products where product_id='123'")
ngx.say(query.get('sql')) --能获取到构造出的sql语句


--query.use()用于指定链接的数据库
query.use(require('config.db'))


--query.select()用于指定要查询的字段，不指定默认为select *
query.select({'id', 'email'})
// 等同于：
query.select('id, email')


--query.from()用于指定要查询的表格
// SELECT * FROM `user`
query.from('user')


--query.where()用于定义 SQL 语句当中的 WHERE 子句。 你可以使用如下三种格式来定义 WHERE 条件：

字符串格式，例如：'status=1' , 这个方法不会自动加引号或者转义。
哈希格式，例如： &#123;status=1,type=2&#125;  ，这个方法将正确地为字段名加引号以及为取值范围转义
操作符格式，例如：&#123;'in', &#123;'2012','2011'&#125;&#125;

操作符格式

操作符格式允许你指定类程序风格的任意条件语句，如下所示：

&#123;操作符, 操作数1, 操作数2, ...&#125;
其中每个操作数可以是字符串格式、哈希格式或者嵌套的操作符格式， 而操作符可以是如下列表中的一个：

and: 操作数会被 AND 关键字串联起来。例如，&#123;'and', 'id=1', 'id=2'&#125; 将会生成 id=1 AND id=2。
如果操作数是一个数组，它也会按上述规则转换成 字符串。例如，&#123;'and', 'type=1', &#123;'or', 'id=1', 'id=2'&#125;&#125; 
将会生成 type=1 AND (id=1 OR id=2)。 这个方法不会自动加引号或者转义。

or: 用法和 and 操作符类似，这里就不再赘述。

between: 第一个操作数为字段名称，第二个和第三个操作数代表的是这个字段 的取值范围。例如，&#123;'between', 'id', 1, 10&#125; 将会生成 id BETWEEN 1 AND 10。

not between: 用法跟between类似

in: 第一个操作数应为字段名称，第二个操作符既是一个数组。 例如， 
&#123;'in', 'id', &#123;1, 2, 3&#125;&#125; 将生成 `id` IN ('1', '2', '3') 
该方法将正确地为字段名加引号以及为取值范围转义

not in: 用法和 in 操作符类似，这里就不再赘述。

--query.orderBy() 方法是用来指定 SQL 语句当中的 ORDER BY 子句的。例如，
// ... ORDER BY create_time desc
query.orderBy('create_time desc')
等价于：query.orderBy{create_time=desc}


--query.groupBy() 方法是用来指定 SQL 语句当中的 GROUP BY 片断的。例如，
// ... GROUP BY `id`, `status`
query.groupBy{'id', 'status'}


--query.limit() 和 query.offset() 是用来指定 SQL 语句当中 的 LIMIT 和 OFFSET 子句的。例如，
// ... LIMIT 10 OFFSET 20   等价于mysql的limit 20,10
query.limit(10).offset(20)
如果你指定了一个无效的 limit 或者 offset（例如，一个负数），那么它将会被忽略掉。


--query.page() 能通过传递"vendor.Pager"对象，设置 LIMIT 和 OFFSET 达到分页查询的目的：
local page = require("vendor.Pager"){
	pageSize = 8,
	totalCount = 10,
}
query.page(page) --等价于：query.limit(page.limit).offset(page.offset)


查询方法

vendor.Query 提供了一整套的用于不同查询目的的方法。

query.all(): 将返回一个由行组成的数组。
query.one(): 返回结果集的第一行。
query.count(): 返回 COUNT 查询的结果。

例如：
local db = require('config.db')
local query = require('vendor.Query'){}.use(db).from('products').where{product_id=123}
local rows = query.all()
local row = query.one()
local count = query.count()


其他方法：
query.insert(): 插入数据。
query.update(): 更新数据。
query.delete(): 删除数据。
query.save(): 新数据调用等价于query.insert()，查询结果集调用等价于query.update()。

例如：
local db = require('config.db')
local data = {product_id=123,product_name='name123'}
local query = require('vendor.Query')(data).use(db).from('products')

--插入：
local row = query.insert()
--要获取新插入的数据的自增长id，请使用：
local id = row.get('insert_id') --等价于query.get('insert_id')

--更新：
row.product_name = 'name456'
row.update()	--或者query.update(row)

--删除：
row.delete()	--或者query.update(row)
</pre>
<p>使用活动记录 (Active Record)</p>
<pre>
vendor.ActiveRecord 进一步封装了查询生成器vendor.Query
同时vendor.ActiveRecord继承于vendor.Model，能够使用模型对象的load(),rules(),hasErrors(),validate()等方法

使用示例：
local Product = require('vendor.ActiveRecord'){
	
	--db = require('config.db'),	--可选属性，指定使用的数据库连接，未设置时默认使用'config.db'

	tableName = function()			--必须实现tableName方法，返回要操作的数据表名称
		return 'products'
	end

}

--插入数据：
local product = Product.new{
	product_id	 = 123,
	product_name = 'name123',
}	--返回的对象支持使用vendor.Query方法
product.save()  --等价于product.insert()

--快速查找一行
local row = Product.findOne{product_id = 123}	--返回的对象支持使用vendor.Query方法

--快速查找多行
local rows = Product.findAll{user='creater'}	--返回的对象支持使用vendor.Query方法

--复杂查询
local query = Product.find().where{user='creater'}	--返回的对象支持使用vendor.Query方法
local page = Pager{
	pageSize = 8,
	totalCount = query.count(),
}
local list = query.orderBy('create_time desc').page(page).all()

--同样可以执行以下操作
query.insert(): 插入数据。
query.update(): 更新数据。
query.delete(): 删除数据。


建议每个数据表格在models目录下建立一个继承于'vendor.ActiveRecord'的操作类
</pre>
<p>数据库安全问题</p>
<pre>
使用vendor.ActiveRecord 或 vendor.Query	自动生成查询语句
在构造sql语句的过程中，如果传递的参数是表格，构造器会进行转义操作，防止sql注入

但如果传递的是字符串，则不会进行转义操作，建议尽量少用，并能确保sql安全
可以通过 query.get('sql') 获取执行的sql语句

如果要进行转义，可使用util.mescape()方法
local util = require("vendor.Util")
value = util.mescape(value)
</pre>
    </div>
</div>



]]